﻿#region Libraries

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using HelixToolkit.Wpf;

#endregion

namespace DriveBeamEditor.Wpf
{
    /// <summary>
    ///     Represents grid lines.
    /// </summary>
    public class GridLines : ModelVisual3D
    {
        #region Dependency properties

        /// <summary>
        ///     The center property.
        /// </summary>
        public static readonly DependencyProperty CenterProperty = DependencyProperty.Register(
            "Center", typeof(Point3D), typeof(GridLines), new UIPropertyMetadata(new Point3D(), GeometryChanged));

        /// <summary>
        ///     The distance property.
        /// </summary>
        public static readonly DependencyProperty DistanceProperty = DependencyProperty.Register(
            "MinorDistance", typeof(double), typeof(GridLines), new PropertyMetadata(1.0, GeometryChanged));

        /// <summary>
        ///     The length direction property.
        /// </summary>
        public static readonly DependencyProperty LengthDirectionProperty =
            DependencyProperty.Register(
                "LengthDirection",
                typeof(Vector3D),
                typeof(GridLines),
                new UIPropertyMetadata(new Vector3D(1, 0, 0), GeometryChanged));

        /// <summary>
        ///     The length property.
        /// </summary>
        public static readonly DependencyProperty LengthProperty = DependencyProperty.Register(
            "Length", typeof(double), typeof(GridLines), new PropertyMetadata(100.0, GeometryChanged));

        /// <summary>
        ///     The major distance property.
        /// </summary>
        public static readonly DependencyProperty MajorDistanceProperty = DependencyProperty.Register(
            "MajorDistance", typeof(double), typeof(GridLines), new PropertyMetadata(10.0, GeometryChanged));

        /// <summary>
        ///     The major line color property.
        /// </summary>
        public static readonly DependencyProperty MajorLineColorProperty = DependencyProperty.Register(
            "MajorLineColor", typeof(Color), typeof(GridLines), new UIPropertyMetadata(Color.FromRgb(140, 140, 140)));

        /// <summary>
        ///     The major line thickness property.
        /// </summary>
        public static readonly DependencyProperty MajorLineThicknessProperty =
            DependencyProperty.Register(
                "MajorLineThickness", typeof(double), typeof(GridLines), new UIPropertyMetadata(1.2));

        /// <summary>
        ///     The minor line color property.
        /// </summary>
        public static readonly DependencyProperty MinorLineColorProperty = DependencyProperty.Register(
            "MinorLineColor", typeof(Color), typeof(GridLines), new UIPropertyMetadata(Color.FromRgb(150, 150, 150)));

        /// <summary>
        ///     The minor line thickness property.
        /// </summary>
        public static readonly DependencyProperty MinorLineThicknessProperty =
            DependencyProperty.Register(
                "MinorLineThickness", typeof(double), typeof(GridLines), new UIPropertyMetadata(1.0));

        /// <summary>
        ///     The normal property.
        /// </summary>
        public static readonly DependencyProperty NormalProperty = DependencyProperty.Register(
            "Normal",
            typeof(Vector3D),
            typeof(GridLines),
            new UIPropertyMetadata(new Vector3D(0, 0, 1), GeometryChanged));

        /// <summary>
        ///     The width property.
        /// </summary>
        public static readonly DependencyProperty WidthProperty = DependencyProperty.Register(
            "Width", typeof(double), typeof(GridLines), new PropertyMetadata(100.0, GeometryChanged));

        /// <summary>
        ///     The x axis line color property.
        /// </summary>
        public static readonly DependencyProperty XAxisLineColorProperty = DependencyProperty.Register(
            "XAxisLineColor", typeof(Color), typeof(GridLines), new UIPropertyMetadata(Color.FromRgb(150, 75, 75)));

        /// <summary>
        ///     The y axis line color property.
        /// </summary>
        public static readonly DependencyProperty YAxisLineColorProperty = DependencyProperty.Register(
            "YAxisLineColor", typeof(Color), typeof(GridLines), new UIPropertyMetadata(Color.FromRgb(75, 150, 75)));

        /// <summary>
        ///     The z axis line color property.
        /// </summary>
        public static readonly DependencyProperty ZAxisLineColorProperty = DependencyProperty.Register(
            "ZAxisLineColor", typeof(Color), typeof(GridLines), new UIPropertyMetadata(Color.FromRgb(75, 75, 150)));

        /// <summary>
        ///     The z axis line draw or hide property.
        /// </summary>
        public static readonly DependencyProperty ShowZAxisProperty = DependencyProperty.Register(
            "ShowZAxis", typeof(bool), typeof(GridLines), new UIPropertyMetadata(false));

        /// <summary>
        ///     Show or hide property.
        /// </summary>
        public static readonly DependencyProperty VisibilityProperty = DependencyProperty.Register(
            "Visibility", typeof(Visibility), typeof(GridLines), new UIPropertyMetadata(Visibility.Visible));

        #endregion

        #region Fields

        /// <summary>
        ///     The length direction.
        /// </summary>
        private Vector3D lengthDirection;

        /// <summary>
        ///     The width direction.
        /// </summary>
        private Vector3D widthDirection;

        #endregion

        #region Properties

        /// <summary>
        ///     Gets or sets the center of the grid.
        /// </summary>
        /// <value> The center. </value>
        public Point3D Center
        {
            get { return (Point3D)GetValue(CenterProperty); }

            set { SetValue(CenterProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the length.
        /// </summary>
        /// <value> The length. </value>
        public double Length
        {
            get { return (double)GetValue(LengthProperty); }

            set { SetValue(LengthProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the length direction.
        /// </summary>
        /// <value> The length direction. </value>
        public Vector3D LengthDirection
        {
            get { return (Vector3D)GetValue(LengthDirectionProperty); }

            set { SetValue(LengthDirectionProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the distance between major grid lines.
        /// </summary>
        /// <value> The major distance. </value>
        public double MajorDistance
        {
            get { return (double)GetValue(MajorDistanceProperty); }

            set { SetValue(MajorDistanceProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the color of the major line.
        /// </summary>
        /// <value> The color of the major line. </value>
        public Color MajorLineColor
        {
            get { return (Color)GetValue(MajorLineColorProperty); }

            set { SetValue(MajorLineColorProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the major line thickness.
        /// </summary>
        /// <value> The major line thickness. </value>
        public double MajorLineThickness
        {
            get { return (double)GetValue(MajorLineThicknessProperty); }

            set { SetValue(MajorLineThicknessProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the distance between minor grid lines.
        /// </summary>
        /// <value> The minor distance. </value>
        public double MinorDistance
        {
            get { return (double)GetValue(DistanceProperty); }

            set { SetValue(DistanceProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the color of the minor line.
        /// </summary>
        /// <value> The color of the minor line. </value>
        public Color MinorLineColor
        {
            get { return (Color)GetValue(MinorLineColorProperty); }

            set { SetValue(MinorLineColorProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the minor line thickness.
        /// </summary>
        /// <value> The minor line thickness. </value>
        public double MinorLineThickness
        {
            get { return (double)GetValue(MinorLineThicknessProperty); }

            set { SetValue(MinorLineThicknessProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the normal of the grid.
        /// </summary>
        /// <value> The normal. </value>
        public Vector3D Normal
        {
            get { return (Vector3D)GetValue(NormalProperty); }

            set { SetValue(NormalProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the width.
        /// </summary>
        /// <value> The width. </value>
        public double Width
        {
            get { return (double)GetValue(WidthProperty); }

            set { SetValue(WidthProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the color of the X axis line.
        /// </summary>
        /// <value> The color of the X axis line. </value>
        public Color XAxisLineColor
        {
            get { return (Color)GetValue(XAxisLineColorProperty); }

            set { SetValue(XAxisLineColorProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the color of the Y axis line.
        /// </summary>
        /// <value> The color of the Y axis line. </value>
        public Color YAxisLineColor
        {
            get { return (Color)GetValue(YAxisLineColorProperty); }

            set { SetValue(YAxisLineColorProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the color of the Z axis line.
        /// </summary>
        /// <value> The color of the Z axis line. </value>
        public Color ZAxisLineColor
        {
            get { return (Color)GetValue(ZAxisLineColorProperty); }

            set { SetValue(ZAxisLineColorProperty, value); }
        }

        /// <summary>
        ///     Gets or sets whether the Z axis should be drawn or not.
        /// </summary>
        /// <value> True to display the Z axis, otherwise false. </value>
        public bool ShowZAxis
        {
            get { return (bool)GetValue(ShowZAxisProperty); }

            set { SetValue(ShowZAxisProperty, value); }
        }

        /// <summary>
        ///     Gets or sets whether the grid should be rendered.
        /// </summary>
        /// <value> Visible to display the grid, Hidden to not display it. </value>
        public Visibility Visibility
        {
            get { return (Visibility)GetValue(VisibilityProperty); }

            set { SetValue(VisibilityProperty, value); }
        }

        #endregion

        #region CTORs

        /// <summary>
        ///     Initializes a new instance of the <see cref="GridLines" /> class.
        /// </summary>
        public GridLines()
        {
            CreateGrid(true);
        }

        #endregion

        #region overrides

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);

            if (e.Property == ShowZAxisProperty ||
                e.Property == MinorLineColorProperty ||
                e.Property == MajorLineColorProperty ||
                e.Property == XAxisLineColorProperty ||
                e.Property == YAxisLineColorProperty ||
                e.Property == ZAxisLineColorProperty ||
                e.Property == WidthProperty ||
                e.Property == LengthProperty ||
                e.Property == VisibilityProperty ||
                e.Property == DistanceProperty ||
                e.Property == MajorDistanceProperty)
            {
                ForceRedraw();
            }
        }

        #endregion

        #region Methods

        protected void CreateGrid(bool showAxes)
        {
            var majorLinePoints = new Point3DCollection();
            var minorLinePoints = new Point3DCollection();
            var xlinePoints = new Point3DCollection();
            var ylinePoints = new Point3DCollection();
            var zlinePoints = new Point3DCollection();

            Children.Clear();

            lengthDirection = LengthDirection;
            lengthDirection.Normalize();
            widthDirection = Vector3D.CrossProduct(Normal, lengthDirection);
            widthDirection.Normalize();

            var minDis = (float)Math.Max(MinorDistance, 0.1);
            var majDis = (float)Math.Max(MajorDistance, 0.1);

            var minX = -(float)Width / 2f - ((float)Width / 2f) % minDis;
            var minY = -(float)Length / 2f - ((float)Length / 2f) % minDis;
            var maxX = (float)Width / 2f - ((float)Width / 2f) % minDis;
            var maxY = (float)Length / 2f - ((float)Length / 2f) % minDis;

            var count = (float)Width / minDis;
            for (var ix = 0; ix <= count / 2; ix++)
            {
                var x = ix * minDis;
                var pc = Math.Abs(Math.Round(ix % majDis, 2)) < float.Epsilon ? majorLinePoints : minorLinePoints;
                if (Math.Abs(x) <= float.Epsilon)
                {
                    if (MinorDistance > double.Epsilon)
                        AddLine(pc, GetPoint(x, minY, 0), GetPoint(x, 0, 0), 1);
                    AddLine(ylinePoints, GetPoint(x, 0, 0.001), GetPoint(x, maxY, 0.001), 1);
                }
                else if (MinorDistance > double.Epsilon)
                {
                    AddLine(pc, GetPoint(x, minY, 0), GetPoint(x, maxY, 0), 1);
                    AddLine(pc, GetPoint(-x, minY, 0), GetPoint(-x, maxY, 0), 1);
                }
            }
            for (var iy = 0; iy <= count / 2; iy++)
            {
                var y = iy * minDis;
                var pc = Math.Abs(iy % majDis) < float.Epsilon ? majorLinePoints : minorLinePoints;
                if (Math.Abs(y) <= float.Epsilon)
                {
                    if (MinorDistance > double.Epsilon)
                        AddLine(pc, GetPoint(minX, y, 0.001), GetPoint(0, y, 0.001), 1);
                    AddLine(xlinePoints, GetPoint(0, y, 0.002), GetPoint(maxX, y, 0.002), 1);
                }
                else if (MinorDistance > double.Epsilon)
                {
                    AddLine(pc, GetPoint(minX, y, 0.001), GetPoint(maxX, y, 0.001), 1);
                    AddLine(pc, GetPoint(minX, -y, 0.001), GetPoint(maxX, -y, 0.001), 1);
                }
            }
            AddLine(zlinePoints, GetPoint(0, 0, 0), GetPoint(0, 0, maxY + 0.001));



            var majorLines = Visibility != Visibility.Visible ? null : new LinesVisual3D
            {
                Color = MajorLineColor,
                Thickness = MajorLineThickness,
                Points = majorLinePoints,
                IsRendering = true
            };

            var minorLines = Visibility != Visibility.Visible ? null : new LinesVisual3D
            {
                Color = MinorLineColor,
                Thickness = MinorLineThickness,
                Points = minorLinePoints,
                IsRendering = true
            };

            var xlines = new LinesVisual3D
            {
                Color = YAxisLineColor,
                Thickness = MajorLineThickness + 0.2,
                Points = xlinePoints,
                IsRendering = true
            };

            var ylines = new LinesVisual3D
            {
                Color = XAxisLineColor,
                Thickness = MajorLineThickness + 0.2,
                Points = ylinePoints,
                IsRendering = true
            };

            var zlines = new LinesVisual3D
            {
                Color = ZAxisLineColor,
                Thickness = MajorLineThickness + 0.2,
                Points = zlinePoints,
                IsRendering = true
            };

            if (Visibility == Visibility.Visible)
            {
                Children.Add(majorLines);
                Children.Add(minorLines);
            }

            Children.Add(xlines);
            Children.Add(ylines);

            if (ShowZAxis) Children.Add(zlines);
        }

        /// <summary>
        ///     Creates the grid.
        /// </summary>
        protected void CreateGrid()
        {
            var majorLinePoints = new Point3DCollection();
            var minorLinePoints = new Point3DCollection();
            var xlinePoints = new Point3DCollection();
            var ylinePoints = new Point3DCollection();
            var zlinePoints = new Point3DCollection();

            Children.Clear();

            lengthDirection = LengthDirection;
            lengthDirection.Normalize();
            widthDirection = Vector3D.CrossProduct(Normal, lengthDirection);
            widthDirection.Normalize();

            var minX = -Width / 2;
            var minY = -Length / 2;
            var maxX = Width / 2;
            var maxY = Length / 2;

            var z = MajorDistance * 1e-8;

            var x = minX;
            var eps = MinorDistance / 10;
            while (x <= maxX + eps)
            {
                var pc = IsMultipleOf(x, MajorDistance) ? majorLinePoints : minorLinePoints;
                if (Math.Abs(x) < double.Epsilon)
                {
                    AddLine(pc, GetPoint(x, minY, z), GetPoint(x, 0, z), 1);
                    AddLine(ylinePoints, GetPoint(x, 0, 2 * z), GetPoint(x, maxY, 2 * z), 1);
                }
                else
                {
                    AddLine(pc, GetPoint(x, minY, z), GetPoint(x, maxY, z), 1);
                }

                x += MinorDistance;
            }

            var y = minY;
            while (y <= maxY + eps)
            {
                var pc = IsMultipleOf(y, MajorDistance) ? majorLinePoints : minorLinePoints;
                if (Math.Abs(y) < double.Epsilon)
                {
                    AddLine(pc, GetPoint(minX, y), GetPoint(0, y), 1);
                    AddLine(xlinePoints, GetPoint(0, y, 2 * z), GetPoint(maxX, y, 2 * z), 1);
                }
                else
                {
                    AddLine(pc, GetPoint(minX, y), GetPoint(maxY, y));
                }

                y += MinorDistance;
            }

            AddLine(zlinePoints, GetPoint(0, 0, 0), GetPoint(0, 0, maxY + eps));


            var majorLines = Visibility != Visibility.Visible ? null : new LinesVisual3D
            {
                Color = MajorLineColor,
                Thickness = MajorLineThickness,
                Points = majorLinePoints,
                IsRendering = true
            };

            var minorLines = Visibility != Visibility.Visible ? null : new LinesVisual3D
            {
                Color = MinorLineColor,
                Thickness = MinorLineThickness,
                Points = minorLinePoints,
                IsRendering = true
            };

            var xlines = new LinesVisual3D
            {
                Color = YAxisLineColor,
                Thickness = MajorLineThickness,
                Points = xlinePoints,
                IsRendering = true
            };

            var ylines = new LinesVisual3D
            {
                Color = XAxisLineColor,
                Thickness = MajorLineThickness,
                Points = ylinePoints,
                IsRendering = true
            };

            var zlines = new LinesVisual3D
            {
                Color = ZAxisLineColor,
                Thickness = MajorLineThickness,
                Points = zlinePoints,
                IsRendering = true
            };

            if (Visibility == Visibility.Visible)
            {
                Children.Add(majorLines);
                Children.Add(minorLines);
            }

            Children.Add(xlines);
            Children.Add(ylines);

            if (ShowZAxis) Children.Add(zlines);
        }

        /// <summary>
        ///     The geometry changed.
        /// </summary>
        /// <param name="d">
        ///     The d.
        /// </param>
        /// <param name="e">
        ///     The e.
        /// </param>
        private static void GeometryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((GridLines)d).OnGeometryChanged();
        }

        /// <summary>
        ///     Determines whether the specified value is a multiple of x.
        /// </summary>
        /// <param name="value">
        ///     The value.
        /// </param>
        /// <param name="x">
        ///     The x value.
        /// </param>
        /// <returns>
        ///     <c>true</c> if the value is a multiple of x; otherwise, <c>false</c> .
        /// </returns>
        private static bool IsMultipleOf(double value, double x)
        {
            var y2 = x * (int)(value / x);
            return Math.Abs(value - y2) < 1e-3;
        }

        /// <summary>
        ///     Adds the line.
        /// </summary>
        /// <param name="pc">
        ///     The pc.
        /// </param>
        /// <param name="p0">
        ///     The p0.
        /// </param>
        /// <param name="p1">
        ///     The p1.
        /// </param>
        /// <param name="divisions">
        ///     The divisions.
        /// </param>
        private void AddLine(Point3DCollection pc, Point3D p0, Point3D p1, int divisions = 10)
        {
            var v = p1 - p0;
            for (var i = 0; i < divisions; i++)
            {
                pc.Add(p0 + v * i / divisions);
                pc.Add(p0 + v * (i + 1) / divisions);
            }
        }

        /// <summary>
        ///     Gets the point at the specified local coordinates.
        /// </summary>
        /// <param name="x">
        ///     The x.
        /// </param>
        /// <param name="y">
        ///     The y.
        /// </param>
        /// <param name="z">
        ///     The z.
        /// </param>
        /// <returns>
        ///     A point.
        /// </returns>
        private Point3D GetPoint(double x, double y, double z = 0)
        {
            return Center + widthDirection * x + lengthDirection * y + Normal * z;
        }

        /// <summary>
        ///     Called when the geometry changed.
        /// </summary>
        private void OnGeometryChanged()
        {
            foreach (LinesVisual3D lines in Children)
            {
                lines.IsRendering = false;
            }

            Children.Clear();
            CreateGrid(true);
        }

        /// <summary>
        ///     Forces the grid lines to refresh its contents and redraw
        /// </summary>
        public void ForceRedraw()
        {
            OnGeometryChanged();
        }

        #endregion
    }
}